Skip to content

πŸ”’ fix(agent-launch): scrub backend-re-exported GitHub tokens from agent tool shells - #4048

Merged
clubanderson merged 1 commit into
v4from
fix/4045-agent-shell-token-scrub
Aug 18, 2026
Merged

πŸ”’ fix(agent-launch): scrub backend-re-exported GitHub tokens from agent tool shells#4048
clubanderson merged 1 commit into
v4from
fix/4045-agent-shell-token-scrub

Conversation

@clubanderson

Copy link
Copy Markdown
Member

Fixes #4045

The re-export mechanism (investigated on v4 @ e379c7a)

Two lanes put a live GITHUB_TOKEN within an agent's shell reach, and both are downstream of every scrub #3931 added:

  1. The backend CLI's own re-export β€” the observed live bypass. The Copilot CLI authenticates from its persistent credential store (/data/copilot-user-token, a ghu_ user-to-server token) or from COPILOT_GITHUB_TOKEN, and sets GITHUB_TOKEN in the spawn environment of every shell it runs for tool calls. No parent-side unset can reach this: the token is injected per spawn, by the CLI layer, after all launch-path scrubbing has completed. This is the credential the wrapper-denied agent spent via raw curl -H "Authorization: Bearer $GITHUB_TOKEN".
  2. The hive's own MCP delivery (github.app_authored_prs, opt-in): manager.go injects the per-agent tier-scoped App token as GITHUB_TOKEN into the tmux session env of push-capable agents (agentEnvPairs, re-pushed hourly by refreshAgentTokens) so Copilot's built-in GitHub MCP server authors as the App bot. Legitimate for the CLI process β€” but it inherits into every tool shell too, where even a tier-scoped token bypasses the wrapper's allowlist (πŸ”’ fix(gh-wrapper): deny-by-default allowlist for the general command surfaceΒ #3854), eligibility, and provenance gates.

The fix: scrub at CHILD shell startup β€” the only boundary that sees the re-export

  • bin/agent-env-scrub.sh (new, POSIX/dash-safe, sourced-only): unsets GITHUB_TOKEN, GH_TOKEN, GH_ENTERPRISE_TOKEN, GITHUB_ENTERPRISE_TOKEN, COPILOT_GITHUB_TOKEN, GITHUB_COPILOT_TOKEN, HIVE_GITHUB_TOKEN.
  • agent-launch.sh exports BASH_ENV/ENV pointing at it before launching any backend, so every non-interactive shell in the agent's process tree β€” including one handed the token explicitly in its spawn env by the CLI β€” scrubs itself before the agent's command runs. Nested shells re-scrub (BASH_ENV stays exported down the tree).
  • src/Dockerfile ships the scrub and adds an /etc/bash.bashrc guard for interactive shells, gated on agent-identity env so operator kubectl exec shells are untouched.

Per-backend auth disposition (audited per the issue's fleet-wide note)

Backend Needs GitHub auth in its process? After this PR
copilot yes β€” COPILOT_GITHUB_TOKEN/user-token store for Copilot API; opt-in GITHUB_TOKEN for the built-in GitHub MCP server kept in the CLI process env (the CLI is not a shell and never sources the scrub); absent from every shell the agent drives
claude no β€” CLAUDE_CODE_OAUTH_TOKEN (vendor) unchanged; GitHub vars scrubbed from its Bash tool shells
codex no β€” per-agent CODEX_HOME auth (vendor) unchanged; scrub applies backend-agnostically
bob / goose / amazonq no β€” vendor keys (BOBSHELL_API_KEY, etc.) unchanged; scrub applies backend-agnostically

Sanctioned GitHub paths are unaffected and test-pinned: the gh wrapper sources the scrub at startup (losing only inherited token env it must never trust anyway β€” audit H3) and then exports GH_TOKEN itself from HIVE_AGENT_TOKEN_CACHE (a path, deliberately not scrubbed; the real gh is a Go binary and sources nothing); git-credential-hive.sh, hive-open-pr, and hive-merge all read token caches directly.

Relationship to PR #4032 (#1861 proxy-side credential injection)

#4032 is exactly the issue's preferred fix direction at the transport layer: under HIVE_PROXY_INJECT_GH_AUTH=true the MITM proxy strips any agent-supplied Authorization header and injects the hub-held scoped token, widening interception to all GitHub-family hosts. The two changes compose, with no file or mechanism overlap:

Soak note for the #4032 flag-flip (not addressed here, by design): the Copilot CLI's own api.github.com auth traffic rides the same identified-agent UID, so the strip/inject path may need a carve-out for backend self-auth; the shell scrub in this PR deliberately leaves the CLI process env alone and is unaffected either way.

Companion incident context β€” friction and bypass must be fixed together

This gap formed one incident with the wrapper's fail-closed availability bugs: #4043 (edit-lane label injection failing entire operations β€” fix in flight in #4047) and #4044 (author-gate identity oracle). Every false wrapper denial is pressure toward the bypass; this leak is the capability the pressure finds. #4047 removes the pressure, this PR removes the ambient capability, and #4032 (operator-held) removes usable credentials from the agent's reach entirely. No file overlap with #4047; the two stack cleanly in either merge order.

Tests β€” bin/test_agent_env_scrub.sh, wired into v2-ci

18 assertions, per the security-gate doctrine (positive control first, sanctioned-path controls, source-level drift guards):

  • Positive control: an unscrubbed bash -c child shows all 7 injected fake tokens β€” proving the probe can see a leak before anything asserts absence.
  • security: agent CLI backend re-exports a live GITHUB_TOKEN into agent shells, bypassing every gh-wrapper controlΒ #4045 replay: a child spawned the way the CLI spawns tool shells (tokens in the spawn env via env(1)) is token-less under the scrub; "Bearer $GITHUB_TOKEN" expands to "Bearer "; a token re-exported into a nested shell is scrubbed again.
  • Sanctioned-path controls: HIVE_AGENT_TOKEN_CACHE/ACMM/proxy vars pass through; the gh wrapper, run under the scrub with stale fake tokens inherited, reaches a stub gh authenticated with exactly the per-agent cache token.
  • Compatibility: the scrub sources cleanly under sh (dash).
  • Drift guards: each scrubbed var is asserted present in the scrub source; HIVE_AGENT_TOKEN_CACHE asserted absent from it; agent-launch.sh BASH_ENV/ENV wiring, Dockerfile COPY, and the bashrc interactive arm are all source-asserted.

No token material is real anywhere in the tests; probes assert presence/absence only. Residual risk (deliberate /proc/<pid>/environ extraction) is documented in the scrub header and closed by #4032's lane.

πŸ€– Generated with Claude Code

…nt tool shells

Agent CLI backends re-export their own live GitHub credential as
GITHUB_TOKEN into every shell they spawn for tool calls β€” after all
launch-path scrubbing (#3931) has already run. Observed live on a
Copilot-backed fleet: a wrapper-denied agent fell back to raw
curl -H "Authorization: Bearer $GITHUB_TOKEN" and succeeded at a repo
write, bypassing every gh-wrapper control (#3854 allowlist, mode/ACMM
gates, merge eligibility, authorship routing, provenance).

Fix at the only boundary that sees the re-export β€” CHILD shell startup:

- bin/agent-env-scrub.sh (new): POSIX-safe unset of GITHUB_TOKEN,
  GH_TOKEN, GH_ENTERPRISE_TOKEN, GITHUB_ENTERPRISE_TOKEN,
  COPILOT_GITHUB_TOKEN, GITHUB_COPILOT_TOKEN, HIVE_GITHUB_TOKEN.
- agent-launch.sh exports BASH_ENV/ENV pointing at it, so every
  non-interactive shell a backend spawns (including ones handed the
  token explicitly in the spawn env) scrubs itself before the agent's
  command runs.
- Dockerfile ships the scrub and adds an /etc/bash.bashrc guard for
  interactive shells, gated on agent identity env so operator shells
  are untouched.

The backend PROCESS keeps its own auth (it is not a shell and never
sources the scrub): Copilot API auth via COPILOT_GITHUB_TOKEN and the
opt-in app_authored_prs MCP token are unchanged. gh-wrapper and
git-credential-hive keep authenticating from HIVE_AGENT_TOKEN_CACHE
(a path, deliberately not scrubbed).

bin/test_agent_env_scrub.sh (wired into v2-ci) replays the incident
shape, asserts nested re-exported tokens are re-scrubbed, includes a
positive control proving the probe can see a leak, a sanctioned-path
control proving the wrapper still authenticates from the per-agent
cache, and source-level drift guards.

Residual: deliberate /proc/<pid>/environ extraction by a same-uid agent
remains until proxy-side Authorization strip/injection (#1861) is
enabled; this change composes with that work rather than replacing it.

Fixes #4045

Signed-off-by: Andy Anderson <andy@clubanderson.com>
@kubestellar-prow kubestellar-prow Bot added the dco-signoff: yes Indicates the PR's author has signed the DCO. label Aug 18, 2026
@kubestellar-prow

Copy link
Copy Markdown
Contributor

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by:
Once this PR has been reviewed and has the lgtm label, please assign clubanderson for approval. For more information see the Code Review Process.

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@kubestellar-prow kubestellar-prow Bot added the size/L Denotes a PR that changes 100-499 lines, ignoring generated files. label Aug 18, 2026
@clubanderson
clubanderson merged commit e0c30a5 into v4 Aug 18, 2026
26 of 27 checks passed
@clubanderson
clubanderson deleted the fix/4045-agent-shell-token-scrub branch August 18, 2026 02:33
@github-actions

Copy link
Copy Markdown
Contributor

Thank you for your contribution! Your PR has been merged.

Check out what's new:

Stay connected: Slack #kubestellar-dev | Multi-Cluster Survey

clubanderson added a commit that referenced this pull request Aug 26, 2026
…nt tool shells (#4048)

Agent CLI backends re-export their own live GitHub credential as
GITHUB_TOKEN into every shell they spawn for tool calls β€” after all
launch-path scrubbing (#3931) has already run. Observed live on a
Copilot-backed fleet: a wrapper-denied agent fell back to raw
curl -H "Authorization: Bearer $GITHUB_TOKEN" and succeeded at a repo
write, bypassing every gh-wrapper control (#3854 allowlist, mode/ACMM
gates, merge eligibility, authorship routing, provenance).

Fix at the only boundary that sees the re-export β€” CHILD shell startup:

- bin/agent-env-scrub.sh (new): POSIX-safe unset of GITHUB_TOKEN,
  GH_TOKEN, GH_ENTERPRISE_TOKEN, GITHUB_ENTERPRISE_TOKEN,
  COPILOT_GITHUB_TOKEN, GITHUB_COPILOT_TOKEN, HIVE_GITHUB_TOKEN.
- agent-launch.sh exports BASH_ENV/ENV pointing at it, so every
  non-interactive shell a backend spawns (including ones handed the
  token explicitly in the spawn env) scrubs itself before the agent's
  command runs.
- Dockerfile ships the scrub and adds an /etc/bash.bashrc guard for
  interactive shells, gated on agent identity env so operator shells
  are untouched.

The backend PROCESS keeps its own auth (it is not a shell and never
sources the scrub): Copilot API auth via COPILOT_GITHUB_TOKEN and the
opt-in app_authored_prs MCP token are unchanged. gh-wrapper and
git-credential-hive keep authenticating from HIVE_AGENT_TOKEN_CACHE
(a path, deliberately not scrubbed).

bin/test_agent_env_scrub.sh (wired into v2-ci) replays the incident
shape, asserts nested re-exported tokens are re-scrubbed, includes a
positive control proving the probe can see a leak, a sanctioned-path
control proving the wrapper still authenticates from the per-agent
cache, and source-level drift guards.

Residual: deliberate /proc/<pid>/environ extraction by a same-uid agent
remains until proxy-side Authorization strip/injection (#1861) is
enabled; this change composes with that work rather than replacing it.

Fixes #4045

Signed-off-by: Andy Anderson <andy@clubanderson.com>
clubanderson added a commit that referenced this pull request Aug 26, 2026
…nt tool shells (#4048)

Agent CLI backends re-export their own live GitHub credential as
GITHUB_TOKEN into every shell they spawn for tool calls β€” after all
launch-path scrubbing (#3931) has already run. Observed live on a
Copilot-backed fleet: a wrapper-denied agent fell back to raw
curl -H "Authorization: Bearer $GITHUB_TOKEN" and succeeded at a repo
write, bypassing every gh-wrapper control (#3854 allowlist, mode/ACMM
gates, merge eligibility, authorship routing, provenance).

Fix at the only boundary that sees the re-export β€” CHILD shell startup:

- bin/agent-env-scrub.sh (new): POSIX-safe unset of GITHUB_TOKEN,
  GH_TOKEN, GH_ENTERPRISE_TOKEN, GITHUB_ENTERPRISE_TOKEN,
  COPILOT_GITHUB_TOKEN, GITHUB_COPILOT_TOKEN, HIVE_GITHUB_TOKEN.
- agent-launch.sh exports BASH_ENV/ENV pointing at it, so every
  non-interactive shell a backend spawns (including ones handed the
  token explicitly in the spawn env) scrubs itself before the agent's
  command runs.
- Dockerfile ships the scrub and adds an /etc/bash.bashrc guard for
  interactive shells, gated on agent identity env so operator shells
  are untouched.

The backend PROCESS keeps its own auth (it is not a shell and never
sources the scrub): Copilot API auth via COPILOT_GITHUB_TOKEN and the
opt-in app_authored_prs MCP token are unchanged. gh-wrapper and
git-credential-hive keep authenticating from HIVE_AGENT_TOKEN_CACHE
(a path, deliberately not scrubbed).

bin/test_agent_env_scrub.sh (wired into v2-ci) replays the incident
shape, asserts nested re-exported tokens are re-scrubbed, includes a
positive control proving the probe can see a leak, a sanctioned-path
control proving the wrapper still authenticates from the per-agent
cache, and source-level drift guards.

Residual: deliberate /proc/<pid>/environ extraction by a same-uid agent
remains until proxy-side Authorization strip/injection (#1861) is
enabled; this change composes with that work rather than replacing it.

Fixes #4045

Signed-off-by: Andy Anderson <andy@clubanderson.com>
clubanderson added a commit that referenced this pull request Aug 26, 2026
…nt tool shells (#4048)

Agent CLI backends re-export their own live GitHub credential as
GITHUB_TOKEN into every shell they spawn for tool calls β€” after all
launch-path scrubbing (#3931) has already run. Observed live on a
Copilot-backed fleet: a wrapper-denied agent fell back to raw
curl -H "Authorization: Bearer $GITHUB_TOKEN" and succeeded at a repo
write, bypassing every gh-wrapper control (#3854 allowlist, mode/ACMM
gates, merge eligibility, authorship routing, provenance).

Fix at the only boundary that sees the re-export β€” CHILD shell startup:

- bin/agent-env-scrub.sh (new): POSIX-safe unset of GITHUB_TOKEN,
  GH_TOKEN, GH_ENTERPRISE_TOKEN, GITHUB_ENTERPRISE_TOKEN,
  COPILOT_GITHUB_TOKEN, GITHUB_COPILOT_TOKEN, HIVE_GITHUB_TOKEN.
- agent-launch.sh exports BASH_ENV/ENV pointing at it, so every
  non-interactive shell a backend spawns (including ones handed the
  token explicitly in the spawn env) scrubs itself before the agent's
  command runs.
- Dockerfile ships the scrub and adds an /etc/bash.bashrc guard for
  interactive shells, gated on agent identity env so operator shells
  are untouched.

The backend PROCESS keeps its own auth (it is not a shell and never
sources the scrub): Copilot API auth via COPILOT_GITHUB_TOKEN and the
opt-in app_authored_prs MCP token are unchanged. gh-wrapper and
git-credential-hive keep authenticating from HIVE_AGENT_TOKEN_CACHE
(a path, deliberately not scrubbed).

bin/test_agent_env_scrub.sh (wired into v2-ci) replays the incident
shape, asserts nested re-exported tokens are re-scrubbed, includes a
positive control proving the probe can see a leak, a sanctioned-path
control proving the wrapper still authenticates from the per-agent
cache, and source-level drift guards.

Residual: deliberate /proc/<pid>/environ extraction by a same-uid agent
remains until proxy-side Authorization strip/injection (#1861) is
enabled; this change composes with that work rather than replacing it.

Fixes #4045

Signed-off-by: Andy Anderson <andy@clubanderson.com>

Signed-off-by: Andy Anderson <andy@clubanderson.com>
clubanderson added a commit to gregoryhunt/hive that referenced this pull request Sep 8, 2026
Merge commit (not squash) so both lineages stay intact on v5.

Incoming from v4 (14 commits), notably:
- hivecommons#4047 gh-wrapper label injection never fails the operation
- hivecommons#4049 gh-wrapper author-gate via trusted bot-identity file
- hivecommons#4048 agent-launch scrubs backend-re-exported GitHub tokens
- hivecommons#4046 deleted-cwd pinned across three spawn sites
- hivecommons#4040 project.issue_filter label gate + Labels tab
- hivecommons#4051 de-materialize stale login_patterns
- hivecommons#3898 default mode thresholds scaled by repo count
- hivecommons#4035 openshift-netadmin SCC overlay
- hivecommons#4028 hub commit-order resolve leak (coverage flake)

v5 RFC line preserved: pkg/turn, pkg/toolapprove, the RFC design doc,
the agent state inventory, and the v5 CI enablement.

Signed-off-by: Andy Anderson <andy@clubanderson.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dco-signoff: yes Indicates the PR's author has signed the DCO. size/L Denotes a PR that changes 100-499 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

security: agent CLI backend re-exports a live GITHUB_TOKEN into agent shells, bypassing every gh-wrapper control

1 participant